import { Sandpack } from '@/components'

# SuspenseQuery

같은 depth에서 Suspense를 발생시키는 것이 무엇인지 명확하게 표현하기 위해 이 컴포넌트들을 제공합니다.

1. data-fetching만을 위한 UserInfo, PostList와 같은 depth를 제거하기 때문에 prop-drilling도 제거됩니다.
2. Suspense, ErrorBoundary의 범위 변경도 간단해집니다. query의 병렬처리도 더 쉽습니다.
3. Page 컴포넌트 내에서 data-fetching을 모두 관장하기 때문에 내부의 컴포넌트는 presentational하므로 컴포넌트를 분리하기 쉽습니다.

```jsx /SuspenseQuery/
import { SuspenseQuery } from '@suspensive/react-query'
import { Suspense, ErrorBoundary } from '@suspensive/react'
import { PostListItem, UserProfile } from '~/components'

const PostsPage = ({ userId }) => (
  <ErrorBoundary fallback={({ error }) => <>{error.message}</>}>
    <Suspense fallback={'loading...'}>
      <SuspenseQuery {...userQueryOptions(userId)}>
        {({ data: user }) => <UserProfile key={user.id} {...user} />}
      </SuspenseQuery>
      <SuspenseQuery
        {...postsQueryOptions(userId)}
        select={(posts) => posts.filter(({ isPublic }) => isPublic)}
      >
        {({ data: posts }) =>
          posts.map((post) => <PostListItem key={post.id} {...post} />)
        }
      </SuspenseQuery>
    </Suspense>
  </ErrorBoundary>
)
```

<Sandpack>

```tsx Example.tsx active
import { useState } from 'react'
import { ErrorBoundary, Suspense } from '@suspensive/react'
import { SuspenseQuery } from '@suspensive/react-query'
import { PostListItem } from './PostListItem'
import { UserProfile } from './UserProfile'
import { postsQueryOptions, userQueryOptions } from './queries'

export const Example = () => {
  const [userId, setUserId] = useState(1)
  const [showAllPosts, setShowAllPosts] = useState(false)

  return (
    <ErrorBoundary fallback={({ error }) => <>{error.message}</>}>
      <button onClick={() => setUserId((id) => id - 1)} disabled={userId === 1}>
        Previous User
      </button>
      <button
        onClick={() => setUserId((id) => id + 1)}
        disabled={userId === 10}
      >
        Next User
      </button>
      <Suspense fallback={<div>Loading...</div>}>
        <SuspenseQuery {...userQueryOptions(userId)}>
          {({ data: user }) => <UserProfile key={user.id} {...user} />}
        </SuspenseQuery>

        <label>
          <input
            type="checkbox"
            checked={showAllPosts}
            onChange={() => setShowAllPosts((showAllPosts) => !showAllPosts)}
          />
          Show All Posts
        </label>

        <SuspenseQuery
          {...postsQueryOptions(userId)}
          select={(posts) =>
            posts.filter(({ isPublic }) => showAllPosts || isPublic)
          }
        >
          {({ data: posts }) => (
            <ol>
              {posts.map((post) => (
                <PostListItem key={post.id} {...post} />
              ))}
            </ol>
          )}
        </SuspenseQuery>
      </Suspense>
    </ErrorBoundary>
  )
}
```

```tsx PostListItem.tsx
import type { Post } from './api'

export const PostListItem = (props: Post) => {
  return (
    <li style={{ marginBottom: '10px' }}>
      <span style={{ color: props.isPublic ? 'green' : 'red' }}>
        {props.isPublic ? 'Public' : 'Private'} Post{' '}
      </span>
      {props.title}
    </li>
  )
}
```

```tsx UserProfile.tsx
import type { User } from './api'

export const UserProfile = (props: User) => {
  return (
    <div style={{ padding: '16px' }}>
      <h1>{props.name}</h1>
      <p>{props.email}</p>
      <p>{props.phone}</p>
    </div>
  )
}
```

```tsx queries.ts
import { queryOptions } from '@suspensive/react-query'
import { getPosts, getUser } from './api'

export const userQueryOptions = (userId: number) =>
  queryOptions({
    queryKey: ['users', userId],
    queryFn: () => getUser(userId),
  })

export const postsQueryOptions = (userId: number) =>
  queryOptions({
    queryKey: ['users', userId, 'posts'],
    queryFn: () => getPosts(userId),
  })
```

```tsx api.ts
export type Post = {
  userId: number
  id: number
  title: string
  body: string
  isPublic: boolean
}

export const getPosts = async (userId: number): Promise<Post[]> => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts?userId=${userId}`
  )

  if (!response.ok) {
    throw new Error('An error occurred')
  }

  const data = await response.json()

  return data.map((post, index) => ({
    ...post,
    isPublic: index % 2 === 1,
  }))
}

export type User = {
  id: number
  name: string
  username: string
  email: string
  address: {
    street: string
    suite: string
    city: string
    zipcode: string
    geo: {
      lat: string
      lng: string
    }
  }
  phone: string
  website: string
  company: {
    name: string
    catchPhrase: string
    bs: string
  }
}

export const getUser = async (id: number): Promise<User> => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/users/${id}`
  )

  if (!response.ok) {
    throw new Error('An error occurred')
  }

  const data = await response.json()

  return data
}
```

</Sandpack>

## 동기: useSuspenseQuery가 명확히 드러나지 않음

기존의 useSuspenseQuery는 훅이기 때문에 부모에 Suspense, ErrorBoundary를 배치하기 위해 UserInfo, PostList와 같은 이름을 가진 컴포넌트를 만들게 합니다.
이것은 UserInfo, PostList 내부에서 던져질 suspense와 error가 있을지 예측하기 어렵게 만듭니다.

```jsx /useSuspenseQuery/
// posts/page.tsx
import { Suspense, ErrorBoundary } from '@suspensive/react'
import { UserInfo } from './components/UserInfo'
import { PostList } from './components/PostList'

const PostsPage = ({ userId }) => (
  <ErrorBoundary fallback={({ error }) => <>{error.message}</>}>
    <Suspense fallback={'loading...'}>
      <UserInfo userId={userId} />
      {/* 내부적으로 Suspense를 발생할 지 예상하기 어렵습니다. */}
      <PostList userId={userId} />
      {/* 내부적으로 Suspense를 발생할 지 예상하기 어렵습니다. */}
    </Suspense>
  </ErrorBoundary>
)
```

```jsx /useSuspenseQuery/
// posts/components/UserInfo.tsx
import { useSuspenseQuery } from '@suspensive/react-query'
import { UserProfile } from '~/components'

// 이 컴포넌트를 사용하는 입장에서는 UserInfo라는 이름만으로는 내부적으로 Suspense를 발생시키는 지 예측할 수 없습니다.
const UserInfo = ({ userId }) => {
  // data-fetching만을 위한 이 컴포넌트를 만들어야 합니다.
  const { data: user } = useSuspenseQuery(userQueryOptions(userId))

  return <UserProfile {...user} />
}
```

```jsx /useSuspenseQuery/
// posts/components/PostList.tsx
import { useSuspenseQuery } from '@suspensive/react-query'
import { PostListItem } from '~/components'

// 이 컴포넌트를 사용하는 입장에서는 PostList라는 이름만으로는 내부적으로 Suspense를 발생시키는 지 예측할 수 없습니다.
const PostList = ({ userId }) => {
  // data-fetching만을 위한 이 컴포넌트를 만들어야 합니다.
  const { data: posts } = useSuspenseQuery({
    ...postsQueryOptions(userId),
    select: (posts) => posts.filter(({ isPublic }) => isPublic),
  })

  return (
    <>
      {posts.map((post) => (
        <PostListItem {...post} />
      ))}
    </>
  )
}
```

### 버전 기록

| Version  | Changes                                                                                                                              |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `v3.0.0` | `networkMode`는 `'always'`로 고정되었습니다. 자세한 내용은 [v3로 마이그레이션하기 문서](./migration/migrate-to-v3.mdx)를 참고하세요. |
